/*
  Mqtt库
  https://github.com/simplyhippy/pubsubclient
  D1 MINI与ESP8266引脚对应短息
  https://wiki.wemos.cc/products:d1:d1_mini
*/

#include <ESP8266WiFi.h>
#include <PubSubClient.h>
#include <ArduinoJson.h>
#include <FS.h>

#define SYS_LED 2

struct Config {
  String DeviceName = "light";
  String DeviceId = "b3ac1e293ec862c1";
  String Userid = "770702827";
  String Token = "dc810a595f7b7abab21a2f00350805e9";

  //下面的无需修改
  String SSID;
  String Passwd;
  String Server = "easylinker.net";
  String ProtocolType = "MQTT";

  //是否Debug
  bool IsDebug = true;
};
Config _config;
WiFiClient _espClient;
PubSubClient client(_espClient, _config.Server, 8885);

bool _lightOneState = false;
int _lightPin = 16;


bool LoadConfig() {
  File configFile = SPIFFS.open("/config.json", "r");
  if (!configFile) {
    Serial.println("Failed to open config file");
    return false;
  }
  StaticJsonDocument<1024> doc;
  DeserializationError error = deserializeJson(doc, configFile);
  if (error) {
    Serial.println("Failed to read config file.");
    return false;
  }
  _config.SSID = doc["SSID"] | "fail";
  _config.Passwd = doc["Passwd"] | "fail";
  if (_config.SSID == "fail" || _config.Passwd == "fail") {
    return false;
  }
  else {
    Serial.println("Load wifi config from spiffs successful.");
    Serial.print("Loaded ssid: ");
    Serial.println(_config.SSID);
    Serial.print("Loaded passwd: ");
    Serial.println(_config.Passwd);
    return true;
  }
}

bool SaveConfig() {
  DynamicJsonDocument doc(1024);
  JsonObject root = doc.to<JsonObject>();
  root["SSID"] = _config.SSID;
  root["Passwd"] = _config.Passwd;
  root["Server"] = _config.Server;
  root["Token"] = _config.Token;

  File configFile = SPIFFS.open("/config.json", "w");
  if (!configFile) {
    Serial.println("Failed to open config file for writing");
    return false;
  }
  if (serializeJson(doc, configFile) == 0) {
    Serial.println("Failed to write to file");
    return false;
  }
  return true;
}

bool ServerWatchDog() {
  if (WiFi.status() != WL_CONNECTED) {
    BaseConfigWifiConnect();
  }
  if (client.connected()) {
    client.loop();
  }
  else
  {
    EasyLinkerMqttConnect();
  }
  return true;
}

void BaseConfigWifiConnect() {
  Serial.println("Use base config to connect wifi.");
  WiFi.mode(WIFI_STA);
  WiFi.begin(_config.SSID, _config.Passwd);
  //连接超时时间，30s后没有连接将会转入SmartConfig
  int timeout = 30000;
  while (WiFi.status() != WL_CONNECTED) {
    Serial.println("Wait to connect wifi...");
    digitalWrite(SYS_LED, LOW);
    delay(150);
    digitalWrite(SYS_LED, HIGH);
    delay(150);
    timeout = timeout - 300;
    if (timeout <= 0) {
      Serial.println("Wifi connect timeout, use smart config to connect...");
      SmartConfigWifiConnect();
      return;
    }
  }
  Serial.println("WiFi connected by base config.");
  Serial.print("SSID:");
  Serial.println(WiFi.SSID());
  Serial.print("IP Address:");
  Serial.println(WiFi.localIP());
  delay(100);
}

void SmartConfigWifiConnect()
{
  Serial.println("Use smart config to connect wifi.");
  WiFi.mode(WIFI_STA);
  WiFi.beginSmartConfig();
  while (1)
  {
    Serial.println("Wait to connect wifi...");
    digitalWrite(SYS_LED, LOW);
    delay(1000);
    digitalWrite(SYS_LED, HIGH);
    delay(1000);
    if (WiFi.smartConfigDone())
    {
      Serial.println("WiFi connected by smart config.");
      Serial.print("SSID:");
      Serial.println(WiFi.SSID());
      Serial.print("IP Address:");
      Serial.println(WiFi.localIP());
      _config.SSID = WiFi.SSID();
      _config.Passwd = WiFi.psk();
      if (!SaveConfig()) {
        Serial.println("Failed to save config");
      }
      else {
        Serial.println("Config saved");
      }
      break;
    }
  }
  delay(100);
}

void Callback(const MQTT::Publish& pub) {
  // handle message arrived
  String topic = pub.topic();
  String payload = pub.payload_string();
  int payloadLength = pub.payload_len();

  //This is debug output
  if (_config.IsDebug) {
    Serial.print("Length:");
    Serial.println(payloadLength);
    Serial.print("Topic:");
    Serial.println(topic);
    Serial.print("Payload:");
    Serial.println(payload);
  }

  DynamicJsonDocument doc(2048);
  DeserializationError error = deserializeJson(doc, payload);
  if (error) {
    Serial.println("Failed to read payload");
    return;
  }
  JsonObject obj = doc.as<JsonObject>();
  String type = obj["Type"].isNull() ? "" : obj["Type"].as<String>();
  String messageId = obj["MessageId"].isNull() ? "" : obj["MessageId"].as<String>();

  if (type == "Success" || type == "Error") {
    if (type == "Success") {
      ErrorMessage(messageId, 0, "Success");
    }
    else {
      ErrorMessage(messageId, obj["Data"]["Code"].as<int>(), obj["Data"]["Message"].as<String>());
    }
  }
  else if (type == "Control") {
    //Control(String messageId, String propertiy, String value)
    if (obj["Data"].isNull()) {
      Control(messageId, obj["Action"].as<String>(), "", "");
    }
    else {
      String  propertiy = "", value = "";
      if (!obj["Data"]["Propertiy"].isNull()) {
        propertiy = obj["Data"]["Propertiy"].as<String>();
      }
      if (!obj["Data"]["Value"].isNull()) {
        value = obj["Data"]["Value"].as<String>();
      }
      Control(messageId, obj["Action"].as<String>(), propertiy, value);
    }
  }
  else if (type == "Query") {
    Query(messageId, obj["Action"].as<String>());
  }
  else if (type == "Msg") {
    if (obj["Data"].isNull() || obj["Data"]["Content"].isNull())
    {
      return;
    }
    Message(messageId, obj["Data"]["Content"].as<String>());
  }
}

void EasyLinkerMqttConnect() {
  Serial.println("Beigin to connect to easylinker server.");
  while (true)
  {
    // If when connect server wifi disconnect,return
    if (WiFi.status() != WL_CONNECTED)
    {
      return;
    }
    Serial.println("Try to connect easylinker server...");
    digitalWrite(SYS_LED, LOW);
    delay(300);
    digitalWrite(SYS_LED, HIGH);
    delay(300);
    if (!client.connected()) {
      if (client.connect(MQTT::Connect(_config.DeviceId)
                         .set_auth(_config.Userid, _config.Token))) {
        Serial.println("Connected to easylinker server.");
        client.set_callback(Callback);
        Subscribe();
        break;
      }
      else {
        Serial.println("Could not connect to easylinker server.");
      }
    }
    delay(3000);
  }
}

void ConnectServer() {
  // Connect wifi
  if (LoadConfig()) {
    BaseConfigWifiConnect();
  }
  else {
    SmartConfigWifiConnect();
  }
  // Connect easylinker server
  EasyLinkerMqttConnect();
  digitalWrite(SYS_LED, HIGH);
}

void Subscribe() {
  if (client.connected())
  {
    // 按照需要订阅以下四个主题中的其中一个或多个，不可订阅其它主题。
    // 前两个是必要的。
    // /server/control/{deviceId}/{userKey}/#
    // /server/query/{deviceId}/{userKey}/#
    // /server/msg/{deviceId}/{userKey}/#
    // /server/heart
    client.subscribe(MQTT::Subscribe()
                     .add_topic("/server/control/" + _config.DeviceId + "/" + _config.Token + "/#")
                     .add_topic("/server/query/" + _config.DeviceId + "/" + _config.Token + "/#")
                     .add_topic("/server/msg/" + _config.DeviceId + "/" + _config.Token + "/#")
                     .add_topic("/server/heart"));
  }
}

void Publish(String topic, String message) {
  if (client.connected())
  {
    if (topic.length() == 0 || message.length() == 0) {
      return;
    }
    if (topic != "/client/control" && topic != "/client/query" && topic != "/client/msg" && topic != "/client/heart") {
      return;
    }
    if (_config.IsDebug) {
      Serial.print("Publish Topic:");
      Serial.println(topic);
      Serial.print("Publish Payload:");
      Serial.println(message);
    }
    client.publish(topic, message);
  }
}

void ErrorMessage(String messageId, int code, String message) {
  Serial.print("Type:");
  if (code == 0) {
    Serial.println("Success");
  }
  else {
    Serial.println("Error");
  }
  Serial.print("MessageId:");
  Serial.println(messageId);
  Serial.print("Code:");
  Serial.println(code);
  Serial.print("Message:");
  Serial.println(message);
}

void Control(String messageId, String action, String propertiy, String value) {
  if (_config.IsDebug) {
    Serial.print("Type:");
    Serial.println("Control");
    Serial.print("MessageId:");
    Serial.println(messageId);
    Serial.print("Action:");
    Serial.println(action);
    Serial.print("Propertiy:");
    Serial.println(propertiy);
    Serial.print("Value:");
    Serial.println(value);
  }

  if (action == "TurnOn" || action == "TurnOff") {
    if (value == "On") {
      //Result state
      StaticJsonDocument<1024> doc;
      doc["Device"] = _config.DeviceName;
      doc["Uid"] = _config.Userid;
      doc["Key"] = _config.Token;
      doc["ProtocolType"] = _config.ProtocolType;
      doc["Type"] = "Control";
      doc["Action"] = action;
      doc["MessageId"] = messageId;
      doc["DeviceId"] = _config.DeviceId;
      doc["Time"] = 0;
      JsonObject data = doc.createNestedObject("Data");
      data["PowerState"] = value;

      String result = "";
      serializeJson(doc, result);
      if (_config.IsDebug) {
        Serial.print("Result Length:");
        Serial.println(result.length());
        Serial.print("Result:");
        serializeJsonPretty(doc, Serial);
        Serial.println();
      }
      if (result.length() > 0) {
        Publish("/client/control", result);
      }

      LightControl();
      _lightOneState = true;
    }
    else if (value == "Off") {
      //result state
      StaticJsonDocument<1024> doc;
      doc["Device"] = _config.DeviceName;
      doc["Uid"] = _config.Userid;
      doc["Key"] = _config.Token;
      doc["ProtocolType"] = _config.ProtocolType;
      doc["Type"] = "Control";
      doc["Action"] = action;
      doc["MessageId"] = messageId;
      doc["DeviceId"] = _config.DeviceId;
      doc["Time"] = 0;
      JsonObject data = doc.createNestedObject("Data");
      data["PowerState"] = value;

      String result = "";
      serializeJson(doc, result);
      if (_config.IsDebug) {
        Serial.print("Result Length:");
        Serial.println(result.length());
        Serial.print("Result:");
        serializeJsonPretty(doc, Serial);
        Serial.println();
      }
      if (result.length() > 0) {
        Publish("/client/control", result);
      }

      LightControl();
      _lightOneState = false;
    }
  }
}

void Query(String messageId, String action) {
  if (_config.IsDebug) {
    Serial.print("Type:");
    Serial.println("Query");
    Serial.print("MessageId:");
    Serial.println(messageId);
    Serial.print("Action:");
    Serial.println(action);
  }

  //result state
  StaticJsonDocument<1024> doc;
  doc["Device"] = _config.DeviceName;
  doc["Uid"] = _config.Userid;
  doc["Key"] = _config.Token;
  doc["ProtocolType"] = _config.ProtocolType;
  doc["Type"] = "Query";
  doc["Action"] = action;
  doc["MessageId"] = messageId;
  doc["DeviceId"] = _config.DeviceId;
  doc["Time"] = 0;

  //Create data
  JsonObject data = doc.createNestedObject("Data");
  JsonArray values = data.createNestedArray("Values");
  //Create a powerstate value and add it to values
  if (_lightOneState) {
    JsonObject value = values.createNestedObject();
    value["Name"] = "powerstate";
    value["Value"] = "On";
  }
  else {
    JsonObject value = values.createNestedObject();
    value["Name"] = "powerstate";
    value["Value"] = "Off";
  }

  String result = "";
  serializeJson(doc, result);
  if (result.length() > 0) {
    Publish("/client/query", result);
  }
}

void Message(String messageId, String msg) {
  if (_config.IsDebug) {
    Serial.print("Type:");
    Serial.println("Query");
    Serial.print("MessageId:");
    Serial.println(messageId);
    Serial.print("Message:");
    Serial.println(msg);
  }
}

void LightControl() {
  //Set light state
  digitalWrite(_lightPin, HIGH);
  delay(160);
  digitalWrite(_lightPin, LOW);
}

void setup() {
  Serial.begin(115200);
  //Set SYS_LED
  pinMode(SYS_LED, OUTPUT);
  //Set ligth pin state
  pinMode(_lightPin, OUTPUT);
  digitalWrite(_lightPin, LOW);

  //Mount FS
  Serial.println("Mounting FS...");
  if (!SPIFFS.begin()) {
    Serial.println("Failed to mount file system");
    return;
  }
  WiFi.hostname("#" + _config.DeviceName + "-EasyLinker");
  //Connect WIFI
  ConnectServer();
}

void loop() {
  //watch dog,if wifi or server disconnected, it will auto reconnect.
  ServerWatchDog();
}
